/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#pragma once

#include <baja/graphics/dx11.1/details/common.hpp>
#include <baja/graphics/dx11.1/graphics_fwd.hpp>
#include <baja/graphics/dx11.1/details/enum_mappings.hpp>

namespace baja { namespace graphics { namespace dx11_1 {

class scene_transformer : public iscene_transformer
{
public:
    scene_transformer() {}

    static std::shared_ptr<scene_transformer> create()
    {
        return std::make_shared<scene_transformer>();
    }

    // sets the transforms for this scene transformer object
    void setTransforms(
        const render_transforms& transforms, 
        const viewport& viewport, 
        const float32 minDepth, 
        const float32 maxDepth
        )
    {
        transforms.cameraToProjectedSpace.applyFully(transforms.worldToCameraSpace, &_worldToProjected);
        transforms.cameraToWorldSpace.apply(transforms.projectedToCameraSpace, &_projectedToWorld);

        _cameraToProjected = transforms.cameraToProjectedSpace;
        _projectedToCamera = transforms.projectedToCameraSpace;

        _halfWidth = viewport.width / 2.0f;
        _halfHeight = viewport.height / 2.0f;
        _minDepth = minDepth;
        _maxDepth = maxDepth;

        m::vector2d centerOffset(_halfWidth, _halfHeight);
        viewport.position.offsetBy(centerOffset, &_viewportCenter);
    }

    // convert pixel to camera space
    m::line3d pixelToCameraSpace(const m::point2d& pixel)
    {
        m::line3d ray;

        ray = this->createRay(pixel);

        m::line3d returnValue;

        _projectedToCamera.apply(ray, &returnValue);

        return returnValue;
    }

    // convert pixel to world space
    m::line3d pixelToWorldSpace(const m::point2d& pixel)
    {
        m::line3d ray;

        ray = this->createRay(pixel);

        m::line3d returnValue;
        _projectedToWorld.apply(ray, &returnValue);

        return returnValue;
    }

    // convert camera to pixel space
    m::point2d cameraSpaceToPixel(const m::point3d& point, float32* zDepth)
    {
        // compute projected point
        m::point4d cameraPoint(point.x, point.y, point.z);
        m::point4d projectedPoint;
        _cameraToProjected.apply(cameraPoint, &projectedPoint);

        // convert into pixel space
        return this->convertIntoPixelSpace(projectedPoint, zDepth);
    }

    // convert world to pixel space
    m::point2d worldSpaceToPixel(const m::point3d& point, float32* zDepth)
    {
        // compute projected point
        m::point4d worldPoint(point.x, point.y, point.z);
        m::point4d projectedPoint;
        _worldToProjected.apply(worldPoint, &projectedPoint);

        // convert into pixel space
        return this->convertIntoPixelSpace(projectedPoint, zDepth);
    }

private:
    // creates the ray from a screen pixel location
    m::line3d createRay(const m::point2d& pixel)
    {
        // convert pixel into a line segment (from near to far planes)
        m::point3d nearPoint;
        nearPoint.x = (pixel.x - _viewportCenter.x) / _halfWidth;
        nearPoint.y = -(pixel.y - _viewportCenter.y) / _halfHeight;
        nearPoint.z = 0.0f;

        m::point3d farPoint;
        farPoint.x = nearPoint.x;
        farPoint.y = nearPoint.y;
        farPoint.z = 1.0f;

        // account for the perspective divide which occurs in the graphics pipeline
        nearPoint.x *= _minDepth;
        nearPoint.y *= _minDepth;
        nearPoint.z *= _minDepth;
            
        farPoint.x *= _maxDepth;
        farPoint.y *= _maxDepth;
        farPoint.z *= _maxDepth;

        m::line3d ray;
        ray.set(nearPoint, farPoint);
        return ray;
    }

    // converts a projected point into pixel space
    m::point2d convertIntoPixelSpace(const m::point4d& point, float32* zDepth)
    {
        // perform perspective divide
        m::point3d postRHW;
        float32 rhw = fabsf(point.w != 0.0f ? 1.0f / point.w : 0.0f);
        postRHW.x = point.x * rhw;
        postRHW.y = point.y * rhw;
        postRHW.z = point.z * rhw;

        m::point2d result;

        // take post RHW point and convert to pixels
        result.x = _viewportCenter.x + (postRHW.x * _halfWidth);
        result.y = _viewportCenter.y + -(postRHW.y * _halfHeight);

        // set the z depth
        *zDepth = postRHW.z;

        return result;
    }

    m::matrix44 _worldToProjected;
    m::matrix44 _projectedToWorld;

    m::matrix44 _cameraToProjected;
    m::matrix44 _projectedToCamera;

    m::point2d _viewportCenter;

    float32 _halfWidth;
    float32 _halfHeight;
    float32 _minDepth;
    float32 _maxDepth;
};

}}} // namespace baja.graphics.dx11_1

namespace baja { namespace graphics {

// creates a scene_transformer object
inline std::shared_ptr<iscene_transformer> createSceneTransformer()
{
    std::shared_ptr<::baja::graphics::dx11_1::scene_transformer> returnValue = ::baja::graphics::dx11_1::scene_transformer::create();
    return std::dynamic_pointer_cast<iscene_transformer>(returnValue);
}

}} // namespace baja.graphics
